package com.zws.cucumber.expr;

import com.datastax.oss.driver.api.core.type.*;
import com.datastax.oss.driver.internal.core.type.PrimitiveType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zws.cucumber.util.colorfullog.ZwsColorfulLogger;
import com.zws.cucumber.var.ZwsVar;
import net.javacrumbs.jsonunit.JsonAssert;
import net.javacrumbs.jsonunit.core.Option;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger;

import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleScriptContext;
import java.math.BigDecimal;
import java.util.*;

import static com.zws.cucumber.expr.ZwsExpr.aliases;
import static com.zws.cucumber.expr.ZwsExpr.registerVerifier;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.*;

/**
 * author: zws
 */
public class ZwsExprBuiltInVerifiers {

    private static final Logger logger = ZwsColorfulLogger.of(ZwsExprBuiltInVerifiers.class);

    public static final ScriptEngineManager factory = new ScriptEngineManager(ZwsExprBuiltInVerifiers.class.getClassLoader());
    public static final ScriptEngine engine = factory.getEngineByName("groovy");

    private static final ObjectMapper objectMapper = new ObjectMapper();

    public static void init() {
        registerVerifier("match_exact", aliases("match"), true,
            ZwsExprBuiltInVerifiers::verifyExact);
        registerVerifier("match_case_ignored", aliases("case_ignored", "ignore_case"),
            ZwsExprBuiltInVerifiers::verifyCaseIgnored);
        registerVerifier("match_not_equal", aliases("not_equal", "not_eq", "not"),
            ZwsExprBuiltInVerifiers::verifyNotEqual);
        registerVerifier("match_start_with", aliases("start_with"),
            ZwsExprBuiltInVerifiers::verifyStartsWith);
        registerVerifier("match_end_with", aliases("end_with"),
            ZwsExprBuiltInVerifiers::verifyEndsWith);
        registerVerifier("match_contained", aliases("contain"),
            ZwsExprBuiltInVerifiers::verifyContains);
        registerVerifier("match_not_contained", aliases("not_contain"),
                ZwsExprBuiltInVerifiers::verifyNotContains);
        registerVerifier("match_regex", aliases("match_r"),
            ZwsExprBuiltInVerifiers::verifyRegex);
        registerVerifier("match_json", aliases("match_j"),
            ZwsExprBuiltInVerifiers::verifyJson);
        registerVerifier("match_expr", aliases("match_e"),
            ZwsExprBuiltInVerifiers::verifyExpr);
        registerVerifier("match_empty", aliases("empty"),
            ZwsExprBuiltInVerifiers::verifyEmpty);
        registerVerifier("match_blank", aliases("blank"),
            ZwsExprBuiltInVerifiers::verifyBlank);
        registerVerifier("match_not_blank", aliases("not_blank"),
            ZwsExprBuiltInVerifiers::verifyNotBlank);
        registerVerifier("match_null", aliases("null"),
            ZwsExprBuiltInVerifiers::verifyNull);
        registerVerifier("match_not_null", aliases("not_null"),
                (ctx, args, pattern, actual) -> verifyNotNull(args, pattern, actual));
        registerVerifier("match_udt", ZwsExprBuiltInVerifiers::verifyUdt);
        registerVerifier("match_greater_than", aliases("gt"), ZwsExprBuiltInVerifiers::verifyGreaterThan);
        registerVerifier("match_greater_than_or_equal_to", aliases("ge"), ZwsExprBuiltInVerifiers::verifyGreaterThanOrEqualTo);
        registerVerifier("match_less_than", aliases("lt"), ZwsExprBuiltInVerifiers::verifyLessThan);
        registerVerifier("match_less_than_or_equal_to", aliases("le"), ZwsExprBuiltInVerifiers::verifyLessThanOrEqualTo);

        registerVerifier("only_save", ZwsExprBuiltInVerifiers::verifyOnlySave);
    }

    private static Pair<Object, Object> verifyExact(List<String> args, String pattern, Object actual) {
        if (!(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertEquals(pattern, actual);
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyCaseIgnored(List<String> args, String pattern, Object actual) {
        if (!(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertEquals(pattern.toUpperCase(), ((String) actual).toUpperCase());
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyNotEqual(List<String> args, String pattern, Object actual) {
        if (actual != null && !(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertNotEquals(pattern, actual);
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyStartsWith(List<String> args, String pattern, Object actual) {
        if (!(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertTrue(((String) actual).startsWith(pattern), "actual string does not start with pattern string");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyEndsWith(List<String> args, String pattern, Object actual) {
        if (!(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertTrue(((String) actual).endsWith(pattern), "actual string does not end with pattern string");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyContains(List<String> args, String pattern, Object actual) {
        if (!(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertTrue(((String) actual).contains(pattern), "actual string does not contain pattern string");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyNotContains(List<String> args, String pattern, Object actual) {
        if (!(actual instanceof String)) {
            actual = Objects.toString(actual);
        }
        assertFalse(((String) actual).contains(pattern), "actual string contains pattern string");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyRegex(List<String> args, String pattern, Object actual) {
        actual = actual.toString();
        assertTrue(((String) actual).matches(pattern), format("'%s' not match '%s'", actual, pattern));
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyJson(List<String> args, String pattern, Object actual) {
        Option[] options = new Option[args.size()];
        if (args.size() > 0) {
            for (int i = 0; i < args.size(); i++) {
                options[i] = Option.valueOf(args.get(i).toUpperCase());
            }
        }
        JsonAssert.assertJsonEquals(pattern, actual, JsonAssert.when(Option.IGNORING_ARRAY_ORDER, options));
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyExpr(List<String> args, String pattern, Object actual) {
        ScriptContext scriptContext = new SimpleScriptContext();
        Map<String, Object> toMerge = new HashMap<>();
        toMerge.put("x", actual);
        ZwsExprUtils.pushVarBlockAndFillAttrs(scriptContext, toMerge);
        if (args.contains("debug")) {
            ZwsExprUtils.printAttributeOfScriptContext(scriptContext);
        }
        try {
            assertTrue((Boolean) engine.eval(pattern, scriptContext), "");
        } catch (Throwable t) {
            logger.debug("", t);
            fail();
        } finally {
            ZwsVar.popProcedureBlock();
        }
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyEmpty(List<String> args, String pattern, Object actual) {
        assertTrue(StringUtils.isEmpty((CharSequence) actual), "");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyBlank(List<String> args, String pattern, Object actual) {
        assertTrue(StringUtils.isBlank((CharSequence) actual), "");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyNotBlank(List<String> args, String pattern, Object actual) {
        assertTrue(StringUtils.isNotBlank((CharSequence) actual), "");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyNull(List<String> args, String pattern, Object actual) {
        Assertions.assertNull(actual, "");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyNotNull(List<String> args, String pattern, Object actual) {
        Assertions.assertNotNull(actual, "");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyUdt(ZwsExprCtx ctx, List<String> args, String pattern, Object actual) {
        DataType dataType = ctx.getDataType();
        Assertions.assertNotNull(dataType, "dataType should not be null");
        Assertions.assertFalse(dataType instanceof PrimitiveType, "dataType should not be PrimitiveType");
        String fixedPattern = fillDefaultValueForPattern(dataType, pattern);
        logger.debug("fixedPattern: {}", fixedPattern);
        return verifyJson(args, fixedPattern, actual);
    }

    private static String fillDefaultValueForPattern(DataType dataType, String pattern) {
        try {
            Object value = objectMapper.readValue(pattern, Object.class);
            fillDefaultValueForDataType(dataType, value);
            return objectMapper.writeValueAsString(value);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void fillDefaultValueForDataType(DataType dataType, Object value) {
        try {
            if (dataType instanceof ListType listType
                    && listType.getElementType() instanceof UserDefinedType udt) {
                fillDefaultValueForListType(udt, (List<?>) value);

            } else if (dataType instanceof SetType setType
                    && setType.getElementType() instanceof UserDefinedType udt) {
                fillDefaultValueForSetType(udt, (Set<?>) value);

            } else if (dataType instanceof MapType mapType
                    && mapType.getValueType() instanceof UserDefinedType udt) {
                fillDefaultValueForMapType(udt, (Map<String, Object>) value);

            } else if (dataType instanceof UserDefinedType udt) {
                fillDefaultValueForUdt(udt, (Map<String, Object>) value);
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void fillDefaultValueForListType(UserDefinedType udt, List<?> list) {
        for (Object v : list) {
            Map<String, Object> m = (Map<String, Object>) v;
            fillDefaultValueForUdt(udt, m);
        }
    }

    private static void fillDefaultValueForSetType(UserDefinedType udt, Set<?> set) {
        for (Object v : set) {
            Map<String, Object> m = (Map<String, Object>) v;
            fillDefaultValueForUdt(udt, m);
        }
    }

    private static void fillDefaultValueForMapType(UserDefinedType udt, Map<String, Object> map) {
        for (Object v : map.values()) {
            Map<String, Object> m = (Map<String, Object>) v;
            fillDefaultValueForUdt(udt, m);
        }
    }

    private static void fillDefaultValueForUdt(UserDefinedType udt, Map<String, Object> patternMap) {
        for (int i = 0; i < udt.getFieldNames().size(); i++) {
            String fieldName = udt.getFieldNames().get(i).asInternal();
            DataType fieldType = udt.getFieldTypes().get(i);
            if (patternMap.containsKey(fieldName)) {
                if (!(fieldType instanceof PrimitiveType)) {
                    fillDefaultValueForDataType(fieldType, patternMap.get(fieldName));
                }

            } else {
                if (fieldType instanceof PrimitiveType) {
                    patternMap.put(fieldName, null);

                } else if (fieldType instanceof ListType) {
                    patternMap.put(fieldName, new ArrayList<>());

                } else if (fieldType instanceof SetType) {
                    patternMap.put(fieldName, new HashSet<>());

                } else if (fieldType instanceof MapType) {
                    patternMap.put(fieldName, new HashMap<>());

                } else if (fieldType instanceof UserDefinedType) {
                    patternMap.put(fieldName, null);

                }
            }
        }
    }

    private static Pair<Object, Object> verifyGreaterThan(List<String> args, String pattern, Object actual) {
        assertTrue(compareActualToPatternInBigDecimal(actual, pattern) > 0,
                "actual[" + actual + "] should be greater than pattern[" + pattern + "]");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyGreaterThanOrEqualTo(List<String> args, String pattern, Object actual) {
        assertTrue(compareActualToPatternInBigDecimal(actual, pattern) >= 0,
                "actual[" + actual + "] should be greater than or equal to pattern[" + pattern + "]");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyLessThan(List<String> args, String pattern, Object actual) {
        assertTrue(compareActualToPatternInBigDecimal(actual, pattern) < 0,
                "actual[" + actual + "] should be less than pattern[" + pattern + "]");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyLessThanOrEqualTo(List<String> args, String pattern, Object actual) {
        assertTrue(compareActualToPatternInBigDecimal(actual, pattern) <= 0,
                "actual[" + actual + "] should be less than or equal to pattern[" + pattern + "]");
        return Pair.of(null, actual);
    }

    private static Pair<Object, Object> verifyOnlySave(List<String> args, String pattern, Object actual) {
        ZwsVar.put(pattern, actual);
        return Pair.of(null, actual);
    }

    private static int compareActualToPatternInBigDecimal(Object actual, String pattern) {
        return new BigDecimal(String.valueOf(actual)).compareTo(new BigDecimal(pattern));
    }

}
